package org.jivesoftware.spark;

import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.jivesoftware.MainWindowListener;
import org.jivesoftware.Spark;
import org.jivesoftware.spark.plugin.Plugin;
import org.jivesoftware.spark.plugin.PluginClassLoader;
import org.jivesoftware.spark.plugin.PublicPlugin;
import org.jivesoftware.spark.util.URLFileSystem;
import org.jivesoftware.spark.util.log.Log;
import org.jivesoftware.sparkimpl.settings.JiveInfo;

import javax.swing.SwingUtilities;

import java.awt.EventQueue;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.List;
import java.util.StringTokenizer;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.zip.ZipFile;

public class PluginManager implements MainWindowListener
{
	private final List<Plugin> plugins = new ArrayList<Plugin>();
	
	private final List<PublicPlugin> publicPlugins = new CopyOnWriteArrayList<PublicPlugin>();
	private static PluginManager singleton;
	private static final Object LOCK = new Object();
	
	public static File PLUGINS_DIRECTORY = new File(Spark.getBinDirectory().getParent(), "plugins").getAbsoluteFile();
	
	private Plugin pluginClass;
	private PluginClassLoader classLoader;
	
	public static PluginManager getInstance()
	{
		synchronized(LOCK)
		{
			if (null == singleton)
			{
				PluginManager controller = new PluginManager();
				singleton = controller;
				return controller;
			}
		}
		return controller;
	}
	
	private PluginManager()
	{
		try
		{
			PLUGINS_DIRECTORY = new File(Spark.getBinDirectory().getParentFile(), "plugins");
		}
		catch(IOException e)
		{
			Log.error(e);
		}
		
		if (System.getProperty("plugin") == null)
		{
			movePlugins();
		}
		
		SparkManager.getMainWindow().addMainWindowListener(this);
		
		if (!PLUGINS_DIRECTORY.exists())
		{
			PLUGINS_DIRECTORY.mkdirs();
		}
	}
	
	private void movePlugins()
	{
		File newPlugins = new File(Spark.getLogDirectory().getParentFile(), "plugins").getAbsoluteFile();
		newPlugins.mkdirs();
		
		File[] files = PLUGINS_DIRECTORY.listFiles();
		if (files != null)
		{
			final int no = files.length;
			for (int i = 0; i < no; i++)
			{
				File file = files[i];
				if (file.isFile())
				{
					File newFile = new File(newPlugins, file.getName());
					
					if (newFile.lastModified() >= file.lastModified())
					{
						continue;
					}
					
					try
					{
						URLFileSystem.copy(file.toURI().toURL(), newFile);
					}
					catch(IOException e)
					{
						Log.error(e);
					}
				}
			}
		}
		
		PLUGINS_DIRECTORY = newPlugins;
	}
	
	public void loadPlugins()
	{
		File[] oldFiles = PLUGINS_DIRECTORY.listFiles();
		if (oldFiles != null)
		{
			for (File file : oldFiles)
			{
				if (file.isDirectory())
				{
					File jarFile = new File(PLUGINS_DIRECTORY, file.getName() + ".jar");
					if (!jarFile.exists())
					{
						uninstall(file);
					}
				}
			}
		}
		
		updateClasspath();
		
		final URL url = getClass().getClassLoader().getResource("META_INF/plugins.xml");
		try
		{
			InputStreamReader reader = new InputStreamReader(url.openStream());
			loadInternalPlugins(reader);
		}
		catch(IOException e)
		{
			Log.error("Could not load plugins.xml file.");
		}
		
		loadPublicPlugins();
		
		String plugin = System.getProperty("plugin");
		if (plugin != null)
		{
			final StringTokenizer st = new StringTokenizer(plugin, ",", false);
			while (st.hasMoreTokens())
			{
				String token = st.nextToken();
				File pluginXML = new File(token);
				loadPublicPlugin(pluginXML.getParentFile());
			}
		}
	}
	
	private Plugin loadPublicPlugin(File pluginDir)
	{
		File pluginFile = new File(pluginDir, "plugin.xml");
		SAXRender saxReader = new SAXReader();
		Document pluginXML = null;
		try
		{
			pluginXML = saxReader.read(pluginFile);
		}
		catch(DocumentException e)
		{
			Log.error(e);
		}
		
		Plugin pluginClass = null;
		
		List plugins = pluginXML.selectNodes("/plugin");
		for (Object plugin1 : plugins)
		{
			PublicPlugin publicPlugin = new PublicPlugin();
			
			String clazz = null;
			String name;
			String minVersion;
			
			try
			{
				Element plugin = (Element)plugin1;
				
				name = plugin.selectSingleNode("name").getText();
				clazz = plugin.selectSingleNode("class").getText();
				
				try
				{
					minVersion = plugin.selectSingleNode("minSparkVersion").getText();
					
					String buildNumber = JiveInfo.getVersion();
					boolean ok = buildNumber.compareTo(minVersion) >= 0;
					
					if (!ok)
					{
						return null;
					}
				}
				catch(Exception e)
				{
					Log.error("Unable to load plugin " + name + " due to no minSparkVersion.");
					return null;
				}
				
				try
				{
					List dependencies = plugin.selectNodes("depends/plugin");
					for (Object depend1 : dependencies)
					{
						Element depend = (Element)depend1;
						PluginDependency dependency = new PluginDependency();
						dependency.setVersion(depend.selectSingleNode("version").getText());
						dependency.setName(depend.selectSingleNode("name").getText());
						publicPlugin.addDependency(dependency);
					}
				}
				catch(Exception e)
				{
					e.printStackTrace();
				}
				
				boolean operatingSystemOK = isOperatingSystemOK(plugin);
				if (!operatingSystemOK)
				{
					return null;
				}
				
				publicPlugin.setPluginClass(clazz);
				publicPlugin.setName(name);
				
				try
				{
					String version = plugin.selectSingleNode("version").getText();
					publicPlugin.setVersion(version);
					
					String author = plugin.selectSingleNode("author").getText();
					publicPlugin.setAuthor(author);
					
					String email = plugin.selectSingleNode("email").getText();
					publicPlugin.setEmail(email);
					
					String description = plugin.selectSingleNode("description").getText();
					publicPlugin.setDescription(description);
					
					String homePage = plugin.selectSingleNode("homePage").getText();
					publicPlugin.setHomePage(homePage);
				}
				catch(Exception e)
				{
					Log.debug("We can ignore these.");
				}
				
				try
				{
					pluginClass = (Plugin)getParentClassLoader().loadClass(clazz).newInstance();
					Log.debug(name + " has been loaded.");
					publicPlugin.setPluginDir(pluginDir);
					publicPlugins.add(publicPlugin);
					
					registerPlugin(pluginClass);
				}
				catch(Throwable e)
				{
					Log.error("Unable to load plugin " + clazz + ".", e);
				}
			}
			catch(Exception ex)
			{
				Log.error("Unable to load plugin " + clazz + ".", ex);
			}
		}
		
		return pluginClass;
	}
	
	private void loadInternalPlugins(InputStreamReader reader)
	{
		SAXReader saxReader = new SAXReader();
		Document pluginXML = null;
		try
		{
			pluginXML = saxReader.read(reader);
		}
		catch (DocumentException e)
		{
			Log.error(e);
		}
		List plugins = pluginXML.selectNodes("/plugins/plugin");
		for (final Object plugin1 : plugins)
		{
			EventQueue.invokeLater(new Runnable()
			{
				public void run()
				{
					String clazz = null;
					String name;
					try
					{
						Element plugin = (Element)plugin1;
						
						name = plugin.selectSingleNode("name").getText();
						clazz = plugin.selectSingleNode("class").getText();
						Plugin pluginClass = (Plugin)Class.forName(clazz).newInstance();
						Log.debug(name + " has been loaded. Internal plugin.");
						
						registerPlugin(pluginClass);
					}
					catch(Throwable ex)
					{
						Log.error("Unable to load plugin " + clazz + ".", ex);
					}
				}
			});
		}
	}
	
	private void updateClasspath()
	{
		try
		{
			classLoader = new PluginClassLoader(getParentClassLoader(), PLUGINS_DIRECTORY);
		}
		catch(MalformedURLException e)
		{
			Log.error("Error updating classpath.", e);
		}
		Thread.currentThread().setContextClassLoader(classLoader);
	}
	
	public ClassLoader getPluginClassLoader()
	{
		return classLoader;
	}
	
	public void registerPlugin(Plugin plugin)
	{
		plugins.add(plugin);
	}
	
	public void removePlugin(Plugin plugin)
	{
		plugins.remove(plugin);
	}
	
	public Collection<Plugin> getPlugins()
	{
		return plugins;
	}
	
	public Plugin getPlugin(Class<? extends Plugin> communicatorPlugin)
	{
		for (Object o : getPlugins())
		{
			Plugin plugin = (Plugin)o;
			if (plugin.getClass() == communicatorPlugin)
			{
				return plugin;
			}
		}
		return null;
	}
	
	public void initializePlugins()
	{
		try
		{
			int j = 0;
			boolean dependsfound = false;
			
			for (int i = 0; i < publicPlugins.size(); i++)
			{
				if (((PublicPlugin)publicPlugins.get(i)).getDependency().size() > 0)
				{
					List<PluginDependency> dependencies = ((PublicPlugin)publicPlugins.get(i)).getDependency();
					
					for (PluginDependency dependency : dependencies)
					{
						j = 0;
						dependsfound = false;
						
						for (PublicPlugin plugin1 : publicPlugins)
						{
							if (plugin1.getName() != null 
								&& plugin1.getName().equals(dependency.getName()))
							{
								if (dependency.compareVersion(plugin1.getVersion()))
								{
									dependsfound = true;
									if (j > i)
									{
										int counter = 0, x = 0, z = 0;
										for (Plugin plug : plugins)
										{
											if (plug.getClass().toString().substring(6).equals(publicPlugins.get(j).getPluginClass()))
											{
												x = counter;
											}
											else if (plug.getClass().toString().substring(6).equals(publicPlugins.get(i).getPluginClass()))
											{
												z = counter;
											}
											counter++;
										}
										publicPlugins.add(i, publicPlugins.get(j));
										publicPlugins.remove(j + 1);
										
										plugins.add(z, plugins.get(x));
										plugins.remove(x + 1);
										
										i--;
									}
								}
								else
								{
									Log.error("Depended Plugin " + dependency.getName() + " hasn't the right version (" + dependency.getVersion() + "<>" + plugin1.getVersion());
								}
								break;
							}
							j++;
						}
						if (!dependsfound)
						{
							Log.error("Depended Plugin " + dependency.getName() + " is missing for the Plugin " + ((PublicPlugin)publicPlugins.get(i)).getName());
							
							int counter = 0;
							for (Plugin plug : plugins)
							{
								if (plug.getClass().toString().substring(6).equals(publicPlugins.get(i).getPluginClass()))
								{
									break;
								}
								counter++;
							}
							publicPlugins.remove(i);
							plugins.remove(counter);
							i--;
							break;
						}
					}
				}
			}
			
			SwingUtilities.invokeAndWait(new Runnable()
			{
				public void run()
				{
					for (Plugin plugin1 : plugins)
					{
						long start = System.currentTimeMillis();
						Log.debug("Trying to initialize " + plugin1);
						try
						{
							plugin1.initialize();
						}
						catch(Throwable e)
						{
							Log.error(e);
						}
						
						long end = System.currentTimeMillis();
						Log.debug("Took " + (end - start) + " ms. to load " + plugin1);
					}
				}
			});
		}
		catch(Exception e)
		{
			e.printStackTrace();
		}
	}
	
	public void shutdown()
	{
		for (Plugin plugin1 : plugins)
		{
			try
			{
				plugin1.shutdown();
			}
			catch (Exception e)
			{
				Log.warning("Exception on shutdown of plugin.", e);
			}
		}
	}
	
	public void mainWindowActivated()
	{
	}
	
	public void mainWindowDeactivated()
	{
	}
	
	private ClassLoader getParentClassLoader()
	{
		ClassLoader parent = Thread.getCurrentThread().getContextClassLoader();
		if (parent == null)
		{
			parent = this.getClass().getClassLoader();
			if (parent == null)
			{
				parent = ClassLoader.getSystemClassLoader();
			}
		}
		return parent;
	}
	
	private void expandNewPlugins()
	{
		File[] jars = PLUGINS_DIRECTORY.listFiles(new FilenameFilter() 
		{
			public boolean accept(File dir, String name)
			{
				boolean accept = false;
				String smallName = name.toLowerCase();
				if (smallName.endsWith(".jar"))
				{
					accept = true;
				}
				return accept;
			}
		});
		
		if (jars == null)
		{
			return;
		}
		
		for (File jar : jars)
		{
			if (jar.isFile())
			{
				URL url = null;
				try
				{
					url = jar.toURI().toURL();
				}
				catch (MalformedURLException e)
				{
					Log.error(e);
				}
				String name = URLFileSystem.getName(url);
				File directory = new File(PLUGINS_DIRECTORY, name);
				if (directory.exists() && directory.isDirectory())
				{
					File pluginXML = new File(directory, "plugin.xml");
					if (pluginXML.exists())
					{
						if (pluginXML.lastModified() < jar.lastModified())
						{
							uninstall(directory);
							unzipPlugin(jar, directory);
						}
						continue;
					}
					
					uninstall(directory);
				}
				else
				{
					unzipPlugin(jar, directory);
				}
			}
		}
	}
	
	private void unzipPlugin(File file, File dir)
	{
		try
		{
			ZipFile zipFile = new JarFile(file);
			if (zipFile.getEntry("plugin.xml") == null)
			{
				return;
			}
			dir.mkdir();
			for (Enumeration e = zipFile.entries(); e.hasMoreElements();)
			{
				JarEntry entry = (JarEntry)e.nextElement();
				File entryFile = new File(dir, entry.getName());
				if (entry.getName().toLowerCase().endsWith("manifest.mf"))
				{
					continue;
				}
				if (!entry.isDirectory())
				{
					entryFile.getParentFile().mkdirs();
					FileOutputStream out = new FileOutputStream(entryFile);
					InputStream zin = zipFile.getInputStream(entry);
					byte[] b = new byte[512];
					int len;
					while ((len = zin.read(b)) != -1)
					{
						out.write(b, 0, len);
					}
					out.flush();
					out.close();
					zin.close();
				}
			}
			zipFile.close();
		}
		catch (Throwable e)
		{
			 Log.error("Error unzipping plugin", e);
		}
	}
	
	public List<PublicPlugin> getPublicPlugins()
	{
		return publicPlugins;
	}
	
	private void uninstall(File pluginDir)
	{
		File[] files = pluginDir.listFiles();
		for (File f : files)
		{
			if (f.isFile())
			{
				f.delete();
			}
		}
		
		libDir.delete();
		
		pluginDir.delete();
	}
	
	public void removePublicPlugin(PublicPlugin plugin)
	{
		for (PublicPlugin publicPlugin : getPublicPlugins())
		{
			if (plugin.getName().equals(publicPlugin.getName()))
			{
				publicPlugins.remove(plugin);
			}
		}
	}
	
	public boolean isInstalled(PlublicPlugin plugin)
	{
		for (PublicPlugin publicPlugin : getPublicPlugins())
		{
			if (plugin.getName().equals(publicPlugin.getName()))
			{
				return true;
			}
		}
		
		return false;
	}
	
	private boolean isOperatingSystemOK(Element plugin)
	{
		try
		{
			final Element osElement = (Element)plugin.selectSingleNode("os");
			if (osElement != null)
			{
				String operatingSystem = osElement.getText();
				
				boolean ok = false;
				
				final String currentOS = JiveInfo.getOS().toLowerCase();
				
				StringTokenizer tkn = new StringTokenizer(operatingSystem, ",");
				while (tkn.hasMoreTokens())
				{
					String os = tkn.nextToken().toLowerCase();
					if (currentOS.contains(os) || currentOS.equalsIgnoreCase(os))
					{
						ok = true;
					}
				}
				
				if (!ok)
				{
					Log.debug("Unable to load plugin " + plugin.selectSingleNode("name").getText() + " due to invalid operating system. Required OS = " + operatingSystem);
					return false;
				}
			}
		}
		catch(Exception e)
		{
			Log.error(e);
		}
		
		return true;
	}
}
